package com.virtual.repository.common.managers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.virtual.repository.common.Constants;
import com.virtual.repository.common.criteria.Criterion;
import com.virtual.repository.common.model.CollectionProperty;
import com.virtual.repository.common.model.Mapping;
import com.virtual.repository.common.model.ModelConfiguration;
import com.virtual.repository.common.model.Property;
import com.virtual.repository.common.operators.CriteriaOperator;
import com.virtual.repository.common.operators.LogicalOperator;
import com.virtual.repository.common.queries.Query;
import com.virtual.repository.common.queries.ResultEntity;
import com.virtual.repository.plugins.api.IDatabasePlugin;
import com.virtual.repository.utils.ReflectionUtil;


public class QueryManager extends AbstractQueryManager {
	
	/**
	 * The object holding the configuration model
	 */
	protected ModelConfiguration modelConfig;
	
	/**
	 * The map holding all initialized drivers to the appropriate databases.
	 */
	protected Map<String, IDatabasePlugin> dbPlugins;
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void initalize(ModelConfiguration modelConfig, Map<String, IDatabasePlugin> plugins) {
		this.modelConfig = modelConfig;
		this.dbPlugins = plugins;
	}

	/**
	 * {@inheritDoc}
	 * @throws Exception 
	 */
	@Override
	public <TYPE extends Object> Collection<TYPE> executeQuery(Query query) throws Exception {
		Query translatedQuery = this.queryParser.translateQuery(query, this.modelConfig);
		System.out.println("Translated query: " + translatedQuery);
		
		QueryExecutionData executionData = this.divideAndPrepareQuery(translatedQuery, true);
		System.out.println("\n\nPREPARED QUERIES: " + executionData.getQueriesAsString() + "\n\n");
		
		List<QueryExecutionResultData> queryResults = this.executeSubqueries(executionData);
		
		Collection<TYPE> result = (Collection<TYPE>) this.mergeAndParseResults(queryResults);
		
		return result;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public <TYPE extends Object> TYPE executeQueryForSingleResult(Query query) throws Exception {
		Query translatedQuery = this.queryParser.translateQuery(query, this.modelConfig);
		System.out.println("Translated query: " + translatedQuery);
		
		QueryExecutionData executionData = this.divideAndPrepareQuery(translatedQuery, false);
		System.out.println("\n\nPREPARED QUERIES: " + executionData.getQueriesAsString() + "\n\n");
		
		List<QueryExecutionResultData> queryResults = this.executeSubqueries(executionData);
		
		TYPE result = (TYPE) this.mergeAndParseResults(queryResults);
		if (result instanceof Collection && ((Collection) result).isEmpty()) {
			result = null;
		}
		
		return result;
	}

	/**
	 * Divides the entire query to the appropriate sub-queries, which will be sent to the drivers.
	 * @param query the entire query, builded by the {@link com.virtual.repository.common.queries.IQueryBuilder}
	 * @param collectionResult - flag indicates whether query must return collection (true) or single object(false)
	 * @return QueryExecutionData - object, which holds information about drivers and queries, which should be sent to
	 * @throws Exception
	 */
	private QueryExecutionData divideAndPrepareQuery(Query query, boolean collectionResult) throws Exception {
		if (!this.modelConfig.getMappings().containsKey(query.getSelectedClass().getName())) {
			throw new IllegalStateException("The class: " + query.getSelectedClass().getName() + " specified in the query was not found in the coinfiguration mapping");
		}
		
		Mapping parentMapping = this.modelConfig.getMappings().get(query.getSelectedClass().getName());
		
		//divide the query
		QueryExecutionData executionData = this.divideQuery(parentMapping, query, collectionResult);
		
		return executionData;
	}
	
	/**
	 * Divides the entire query into the sub-queries to the appropriate databases
	 * @param parentMapping - the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						for the parent object(object which has these fields)
	 * @param translatedQuery - the translated entire query
	 * @param collectionResult - flag indicates whether query must return collection (true) or single object(false)  
	 * @return QueryExecutionData - the object, which holds information about query execution, 
	 * 		   especially it holds the sub-queries from the division
	 * @throws Exception
	 */
	private QueryExecutionData divideQuery(Mapping parentMapping, Query translatedQuery, boolean collectionResult) throws Exception {

		Query newQuery = new Query();
		newQuery.setSelectedClass(Class.forName(parentMapping.getClassName()));
		newQuery.setSelectionCriteria(translatedQuery.getSelectionCriteria());
		newQuery.setEntityName(parentMapping.getDbEntityName());
		
		IDatabasePlugin plugin = this.dbPlugins.get(parentMapping.getDataSource().getImplementationClass());

		QueryExecutionData queryExecutionData = new QueryExecutionData();
		queryExecutionData.setExecutionQuery(newQuery);
		queryExecutionData.setExecutionPlugin(plugin);
		
		if (collectionResult) {
			queryExecutionData.setExpectedCollectionResultClass(Constants.DEFAULT_COLLECTION);
		}
		queryExecutionData.setExpectedResultClass(Class.forName(parentMapping.getClassName()));

		for (String selectionFieldFullName : translatedQuery.getSelectedFields()) {
			this.prepareQueryExecutionData(selectionFieldFullName, parentMapping, queryExecutionData);
		}
		
		return queryExecutionData;
	}
	
	/**
	 * Recursive method.
	 * Builds the hierarchy of the query executions. 
	 * It classifies the given selection field name to the appropriate query.
	 * 
	 * @param selectionFieldFullName - the name of the selection field.
	 * @param parentMapping - the mapping between the object field's names and the names in the entity/table defined in the database,
	 * 						  for the parent object(object which has these fields)
	 * @param queryExecution - the root query execution object
	 * @throws Exception
	 */
	private void prepareQueryExecutionData(String selectionFieldFullName, Mapping parentMapping, QueryExecutionData queryExecution) throws Exception {
		String[] selectionFieldNames = selectionFieldFullName.split(Constants.NESTED_FIELDS_SEPARATOR_REGEX);
		
		if (selectionFieldNames.length == 1) {//this is the single property name
			queryExecution.getExecutionQuery().getSelectedFields().add(selectionFieldFullName);
			
		} else {//this is the complex property
			QueryExecutionData newQEData = new QueryExecutionData();
			Mapping newParentMapping = null;
			
			//check if property is single property or maybe collection property
			Property prop = parentMapping.getProperty(selectionFieldNames[0]);
			if (prop == null) {
				CollectionProperty collProp = parentMapping.getCollectionProperty(selectionFieldNames[0]);
				if (collProp != null) {
					newQEData.setParentObjectFieldName(collProp.getName());
					newQEData.setExpectedCollectionResultClass(Class.forName(collProp.getClassName()));
					newParentMapping = modelConfig.getMapping(collProp.getPropertyClass());
				}
			} else {
				newParentMapping = modelConfig.getMapping(prop.getClassName());
				newQEData.setParentObjectFieldName(prop.getName());
			}
			
			
			if (!queryExecution.getSubExecutionData().containsKey(newParentMapping.getClassName())) {
				Query newQuery = new Query();
				newQuery.setSelectedClass(Class.forName(newParentMapping.getClassName()));
				newQuery.setEntityName(newParentMapping.getDbEntityName());
				
				IDatabasePlugin plugin = this.dbPlugins.get(newParentMapping.getDataSource().getImplementationClass());
				
				newQEData.setExecutionQuery(newQuery);
				newQEData.setExecutionPlugin(plugin);
				newQEData.setExpectedResultClass(Class.forName(newParentMapping.getClassName()));
				
				queryExecution.getSubExecutionData().put(newParentMapping.getClassName(), newQEData);
			}
			
			QueryExecutionData readQE = queryExecution.getSubExecutionData().get(newParentMapping.getClassName());
			String newSelectionFieldName = selectionFieldFullName.substring(selectionFieldFullName.indexOf(Constants.NESTED_FIELDS_SEPARATOR) + 1);
			this.prepareQueryExecutionData(newSelectionFieldName, newParentMapping, readQE);
		}
	}
	
	/**
	 * Recursive method.
	 * Executes prepared queries and set the results to the appropriate object inside <code>executions</code> list.
	 * @param executions the list of the query execution mappings
	 * @param multiple parameter defining if the queries should return collection or single object. 
	 * 		  The <code>true</code> value means return the collection.
	 * @throws Exception 
	 * @throws ClassNotFoundException 
	 */
	protected List<QueryExecutionResultData> executeSubqueries(QueryExecutionData executionData) throws Exception {
		List<QueryExecutionResultData> results = new ArrayList<>();
		
		// just execute the final query
		Collection<ResultEntity> subResult = executionData.getExecutionPlugin().executeQuery(executionData.getExecutionQuery());

		// check if type of the result is consistent with declaration, if no,
		// then throw exception and terminate selection
		if (executionData.isCollectionResult()) {
			for (ResultEntity singleEntity : subResult) {
				QueryExecutionResultData resultData = new QueryExecutionResultData(executionData, singleEntity);
				results.add(resultData);
			}
		} else {
			if (subResult != null && subResult.size() > 1) {
				throw new Exception("Query: " + executionData.getExecutionQuery() + " returned too many records! [expected: 1, returned: " + subResult.size() + "]");
			}

			if (subResult != null && !subResult.isEmpty()) {
				QueryExecutionResultData resultData = new QueryExecutionResultData(executionData, subResult.toArray(new ResultEntity[0])[0]);
				results.add(resultData);
			}
		}
		
		if (executionData.hasSubqueries()) {//prepare and execute sub-queries
			//first, call query to get identifiers of root entity
			Mapping parentMapping = this.modelConfig.getMapping(executionData.getExpectedResultClass().getName());
			Property identificationProp = parentMapping.getIdentifierProperty();
			String identificationFieldName = identificationProp.getSourceName();
			Query identificationQuery = executionData.getIdentificationQuery(identificationFieldName);
			
			//get identifiers and check if the returned number is consistent with the type of the query
			Collection<ResultEntity> resultIdentifiers = executionData.getExecutionPlugin().executeQuery(identificationQuery);
			if (resultIdentifiers != null && resultIdentifiers.size() > 1 && !executionData.isCollectionResult()) {
				throw new Exception("Identification query: " + identificationQuery + " returned too many results [expected: 1, returned: " + resultIdentifiers.size() + "]");
			}
				
			//next, for each identifier execute all sub-queries and set appropriate foreign key condition 
			for (int idnNum = 0; idnNum < resultIdentifiers.size(); idnNum++) {
				QueryExecutionResultData resultDataTmp = results.get(idnNum);
				Object idnValue = resultIdentifiers.toArray(new ResultEntity[0])[idnNum].getField(identificationProp.getSourceName());
				
				for (QueryExecutionData subQueryExecData : executionData.getSubExecutionData().values()) {
					//prepare foreign key name
					Mapping subQueryMapping = this.modelConfig.getMapping(subQueryExecData.getExpectedResultClass().getName());
					String foreignKey = parentMapping.getId() + "." + identificationProp.getName();
					Property foreignKeyProp = subQueryMapping.getForeignKeys().get(foreignKey);
					if (foreignKeyProp == null) {
						System.out.println("Foreign key name for property: " + subQueryExecData.getExpectedResultClass() + " not found!");
						continue;
					}
					
					//remember original criteria
					List<Criterion> originalCritria = new ArrayList<>(subQueryExecData.getExecutionQuery().getSelectionCriteria());
					
					//add criterion for foreign key
					Criterion criterion = null;
					if (subQueryExecData.getExecutionQuery().getSelectionCriteria() != null && !subQueryExecData.getExecutionQuery().getSelectionCriteria().isEmpty()) {
						criterion = new Criterion(LogicalOperator.AND, foreignKeyProp.getSourceName(), CriteriaOperator.EQUALS, idnValue);
					} else {
						criterion = new Criterion(foreignKeyProp.getSourceName(), CriteriaOperator.EQUALS, idnValue);
					}
					subQueryExecData.getExecutionQuery().getSelectionCriteria().add(criterion);
					
					//recursive invocation
					resultDataTmp.getSubQueryResults().addAll(this.executeSubqueries(subQueryExecData));
					
					//restore original criteria
					subQueryExecData.getExecutionQuery().setSelectionCriteria(originalCritria);
				}
			}
		} 
		
		return results;
	}
	

	/**
	 * Merges and parses the result saved in <code>QueryExecutionData</code> to the single object, which was specified in the
	 * entire query.
	 * @param executions the list of the query executions data, especially with the results returned by the drivers
	 * @return 
	 * @throws Exception
	 */
	private Object mergeAndParseResults(List<QueryExecutionResultData> queryResults) throws Exception {
		return this.mergeAndParseResults(queryResults, null);
	}

	/**
	 * Merges and parses the result saved in <code>QueryExecutionResultData</code> to the single object, which was specified in the
	 * entire query.
	 * @param queryResults the list of the query results data, especially with the results returned by the drivers
	 * @return 
	 * @throws Exception
	 */
	protected Object mergeAndParseResults(List<QueryExecutionResultData> queryResults, Object parentObject) throws Exception {
		Collection<Object> finalResults = (Collection<Object>) Constants.DEFAULT_COLLECTION.newInstance();
		
		for (QueryExecutionResultData resultData : queryResults) {
			Object resultObj = null;
			if (parentObject == null) {
				resultObj = resultData.getResultClass().newInstance();
			} else {
				resultObj = parentObject;
			}

			if (resultData.hasSubResults()) {
				Map<String, Collection<Object>> collectionsResults = new HashMap<>();
				for (QueryExecutionResultData subQueryResult : resultData.getSubQueryResults()) {
					Object collResultObj = subQueryResult.getResultClass().newInstance();

					this.mergeSingleResult(subQueryResult, collResultObj, subQueryResult.getQueryResult());
					
					// if result had sub-queries
					if (subQueryResult.hasSubResults()) {
						this.mergeAndParseResults(subQueryResult.getSubQueryResults(), collResultObj);
					}
					
					if (subQueryResult.isCollectionResult()) {
						if (!collectionsResults.containsKey(subQueryResult.getParentObjectFieldName())) {
							Collection<Object> collectionResult = (Collection<Object>) subQueryResult.getCollectionResultClass().newInstance();
							collectionsResults.put(subQueryResult.getParentObjectFieldName(), collectionResult);
						}
						Collection<Object> collectionResult = collectionsResults.get(subQueryResult.getParentObjectFieldName());
						collectionResult.add(collResultObj);
						
						// set collection to the parent object
						ReflectionUtil.setField(resultObj, subQueryResult.getParentObjectFieldName(), collectionResult, Constants.NESTED_FIELDS_SEPARATOR_REGEX);
					} else {
						
						// set result object to the parent object
						ReflectionUtil.setField(resultObj, subQueryResult.getParentObjectFieldName(), collResultObj, Constants.NESTED_FIELDS_SEPARATOR_REGEX);
					}
				}
			} else if (resultData.hasSingleResult()) {
				this.mergeSingleResult(resultData, resultObj, resultData.getQueryResult());
			}
			
			if (!resultData.isCollectionResult()) {
				return resultObj;
			}
			
			finalResults.add(resultObj);
		}
		
		return finalResults;
	}

	protected void mergeSingleResult(QueryExecutionResultData queryResult, Object resultObj, ResultEntity resultEntity) throws Exception {
		// parse result
		System.out.println("INFO: Parsing results for ResultEntity: " + resultEntity);
		Object queryResultObject = this.queryParser.parseSingleResult(resultEntity, this.modelConfig, queryResult.getResultClass());

		if (queryResultObject.getClass().equals(resultObj.getClass())) {
			ReflectionUtil.copyFields(resultObj, queryResultObject);
		} else {
			ReflectionUtil.setField(resultObj, queryResult.getParentObjectFieldName(), queryResultObject, Constants.NESTED_FIELDS_SEPARATOR_REGEX);
		}
	}
	
	public ModelConfiguration getModelConfig() {
		return modelConfig;
	}

	public void setModelConfig(ModelConfiguration modelConfig) {
		this.modelConfig = modelConfig;
	}

	public Map<String, IDatabasePlugin> getDbPlugins() {
		return dbPlugins;
	}

	public void setDbPlugins(Map<String, IDatabasePlugin> dbPlugins) {
		this.dbPlugins = dbPlugins;
	}
}
